bitcoin中的密钥综述
关于bitcon中使用的椭圆曲线加密体制的一些事实:
- 私钥长度
32bytes
- 公钥长度
64bytes (未压缩形式)
或者32bytes(压缩形式)+ 1byte(前缀)
- 椭圆曲线C是
secp256k1
曲线 - 椭圆曲线加密体制基于
模运算
在本文中,我们唯一的输入就是私钥
。公钥可以唯一地从私钥推导而来。我们先使用openssl命令行生成一个密钥对样例,然后再尝试编写C语言代码进行同样的操作。
在OpenSSL命令行中生成
私钥
一个私钥是一个随机选取的32 bytes的数字,并且我们都知道32 bytes存储的数字可以转换成为一个非常大的数值,最大可以达到2
256
。去猜测这样一个数字是很荒谬的,因此可以认为它的生成具有很高的随机性。
得到一个全新的私钥是非常容易的:
$ openssl ecparam -name secp256k1 -genkey -out ec-priv.pem
生成的结果文件ec-prive.pem
中包含了曲线名称(secp256k1)和私钥,它们连同一些其它字符一起被base64
编码。
这个文件可以被快速解码为可读的16进制形式:
openssl ec -in ec-priv.pem -text -noout
以下是我所生成的密钥对形式(你生成的与我的不相同):
read EC key
Private-Key: (256 bit)
priv:
16:26:07:83:e4:0b:16:73:16:73:62:2a:c8:a5:b0:
45:fc:3e:a4:af:70:f7:27:f3:f9:e9:2b:dd:3a:1d:
dc:42
pub:
04:82:00:6e:93:98:a6:98:6e:da:61:fe:91:67:4c:
3a:10:8c:39:94:75:bf:1e:73:8f:19:df:c2:db:11:
db:1d:28:13:0c:6b:3b:28:ae:f9:a9:c7:e7:14:3d:
ac:6c:f1:2c:09:b8:44:4d:b6:16:79:ab:b1:d8:6f:
85:c0:38:a5:8c
ASN1 OID: secp256k1
其中私钥被显示为这样的形式会更直观:
16 26 07 83 e4 0b 16 73
16 73 62 2a c8 a5 b0 45
fc 3e a4 af 70 f7 27 f3
f9 e9 2b dd 3a 1d dc 42
这个私钥代表着你的身份,必须安全地将它保存。也就是说,如果这不是我用来举例的而生成的私钥,我不会将它分享给任何人。我们会用这个私钥来签名我们的信息,这样的话所有人就会相信这个信息确实是我本人发出的。如果其他人窃取了你的私钥,他将能够伪造你的身份。在bitcoin中,他就能够拿走你所有的钱。一定要当心!
公钥
默认情况下,一个公钥是由两个
32bytes 的大数组成,这种就是所谓的未压缩形式
。这两个数字代表着二维坐标系上,secp256k1
椭圆曲线上的一个点(x,y),它是符合以下方程的:
y2 = x3 + 7
这个点的坐标是由私钥决定的,但是反之,从点的坐标推断私钥是不可行的。毕竟,这就是椭圆曲线加密算法安全性的保障。根据依赖特性,结合上述方程和一个x值,可以计算得到一个对应的y值。事实上,压缩形式
的公钥就是根据这个原理将y
值省略掉来节省空间的。
我们可以将上述密钥对中的公钥部分取出,存储到一个叫做ec-pub.pem
的外部文件中:
$ openssl ec -in ec-priv.pem -pubout -out ec-pub.pem
接着将它解码:
$ openssl ec -in ec-pub.pem -pubin -text -noout
未包含私钥部分的文本形式就会显示出来:
read EC key
Private-Key: (256 bit)
pub:
04:82:00:6e:93:98:a6:98:6e:da:61:fe:91:67:4c:
3a:10:8c:39:94:75:bf:1e:73:8f:19:df:c2:db:11:
db:1d:28:13:0c:6b:3b:28:ae:f9:a9:c7:e7:14:3d:
ac:6c:f1:2c:09:b8:44:4d:b6:16:79:ab:b1:d8:6f:
85:c0:38:a5:8c
ASN1 OID: secp256k1
一个更加直观的版本:
04
82 00 6e 93 98 a6 98 6e
da 61 fe 91 67 4c 3a 10
8c 39 94 75 bf 1e 73 8f
19 df c2 db 11 db 1d 28
13 0c 6b 3b 28 ae f9 a9
c7 e7 14 3d ac 6c f1 2c
09 b8 44 4d b6 16 79 ab
b1 d8 6f 85 c0 38 a5 8c
这个未压缩
的版本包含了65个字节:
- 一个不变的
04
前缀 - 32 bytes 的 x 坐标
- 32 bytes 的 y 坐标
很容易就可以将它转换为压缩形式
。我们只需省略掉y并改掉它的前缀。这个新前缀是根据y来决定的:前缀是 02
表示y是偶数值,前缀是 03
表示y是奇数值。
通过:
$ openssl ec -in ec-pub.pem -pubin -text -noout -conv_form compressed
命令行中显示:
read EC key
Private-Key: (256 bit)
pub:
02:82:00:6e:93:98:a6:98:6e:da:61:fe:91:67:4c:
3a:10:8c:39:94:75:bf:1e:73:8f:19:df:c2:db:11:
db:1d:28
ASN1 OID: secp256k1
结论, 压缩形式
占据33bytes:
- 一个
02
或03
的前缀 - 32bytes 的 x 坐标
从代码中生成
(注:原文作者的 repository)
密钥对的产生过程是冗长的,然而使用OpenSSL来完成却不难。我ec.h
中声明了一个帮助函数,原型如下:
EC_KEY *bbp_ec_new_keypair(const uint8_t *priv_bytes);
我们来一起分析一下原文中的部分代码,一些OpenSSL中的数据结构如下:
- BN_CTX, BIGNUM
- EC_KEY
- EC_GROUP, EC_POINT
前两个结构体
属于OpenSSL的任意精度算数(大数)部分,因为我们需要处理非常大的数字。EC_KEY
可以是一个完整的密钥对或者是一个单独的公钥。EC_GROUP
和EC_POINT
帮助我们根据私钥来计算公钥。
最重要的部分,我们初始化了一个EC_KEY
结构来存储一对密钥对:
key = EC_KEY_new_by_curve_name(NID_secp256k1);
填充私钥是很容易的,但是需要一个过渡过程。再将要输入的priv_bytes
填入密钥对之前,我们需要将它转换为BIGNUM
,在这里命名为priv
。
BN_init(&priv);
BN_bin2bn(priv_bytes, 32, &priv);
EC_KEY_set_private_key(key, &priv);
对于复杂的大数操作,OpenSSL需要一个上下文环境,这就是BN_CTX
需要被创建的原因。公钥的推导需要一个深层次的数学理解,而这并不是这篇文章所要达到的目标。简单来说,就是我们在曲线上定位一个固定的点G(generator, 代码中的group
), 然后乘以标量私钥n,而这是一个在模运算中不可逆的过程。它的结果P = n * G就是第二个点,公钥pub
。最终,公钥被载入到密钥对中:
ctx = BN_CTX_new();
BN_CTX_start(ctx);
group = EC_KEY_get0_group(key);
pub = EC_POINT_new(group);
EC_POINT_mul(group, pub, &priv, NULL, NULL, ctx);
EC_KEY_set_public_key(key, pub);
最后时刻,我们要在ex-ec-keypair.c
测试密钥对。我们期待如果给定一个固定的私钥,通过代码产生的结果和命令行中产生的结果相同。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。